﻿//-----------------------------------------------------------------------
// <copyright file="XmlFileProcessor.cs" company="Anurag Chatterjee">
//     Copyright (c) Anurag Chatterjee. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace WordsBase
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Xml;
    using System.Xml.XPath;

    /// <summary>
    /// The XML file processor
    /// </summary>
    public class XmlFileProcessor : WordProcessorBase
    {
        /// <summary>
        /// The XML file location
        /// </summary>
        private string xmlFileLocation;

        /// <summary>
        /// Initializes a new instance of the <see cref="XmlFileProcessor"/> class.
        /// </summary>
        /// <param name="xmlFileLocation">The XML file location.</param>
        public XmlFileProcessor(string xmlFileLocation)
        {
            if (!string.IsNullOrWhiteSpace(xmlFileLocation))
            {
                this.xmlFileLocation = xmlFileLocation;
                var wordListFromXmlFile = this.ParseXmlFile();
                if (null != wordListFromXmlFile && wordListFromXmlFile.Any())
                {
                    this.WordsList = wordListFromXmlFile;
                }

                this.AddWordToRootInSource = this.AddWordToRootInSourceXml;
                this.DeleteWordFromRootInSource = this.DeleteWordFromRootListInSourceXml;
                this.UpdateWordInRootInSource = this.UpdateWordInXmlSource;
            }
        }

        /// <summary>
        /// Adds the word to root in source XML.
        /// </summary>
        /// <param name="root">The root.</param>
        /// <param name="word">The word.</param>
        /// <returns>Whether the operation was a success</returns>
        private bool AddWordToRootInSourceXml(string root, string word)
        {
            var validationResult = this.PerformBasicValidationsOnInputParameters(root, word);
            if (!validationResult)
            {
                return false;
            }

            try
            {
                var doc = new XmlDocument();
                doc.Load(this.xmlFileLocation);
                XmlNode wordsNode = doc.SelectSingleNode(XmlFileProcessorConstants.BaseNodeIdentifier);
                if (null != wordsNode)
                {
                    XmlNodeList rootWordNodeList = wordsNode.SelectNodes(XmlFileProcessorConstants.RootNodeIdentifier);
                    if (null != rootWordNodeList && rootWordNodeList.Count > 0)
                    {
                        XmlNode matchingRootWordNode = null;
                        foreach (XmlNode rootWordNode in rootWordNodeList)
                        {
                            if (null != rootWordNode && null != rootWordNode.Attributes)
                            {
                                string rootWord =
                                    rootWordNode.Attributes.GetNamedItem(XmlFileProcessorConstants.ValueAttributeName)
                                        .Value;
                                if (string.Compare(rootWord, root, false, CultureInfo.CurrentCulture) == 0)
                                {
                                    ////the root node already exists
                                    matchingRootWordNode = rootWordNode;
                                    break;
                                }
                            }
                        }

                        ////create the root word node also since the provided root does not exist
                        if (null == matchingRootWordNode)
                        {
                            XmlNode newRootWordNode = doc.CreateNode("element", XmlFileProcessorConstants.RootNodeIdentifier, string.Empty);
                            XmlAttribute rootWordNodeAttribute = doc.CreateAttribute(XmlFileProcessorConstants.ValueAttributeName);
                            rootWordNodeAttribute.Value = root;
                            newRootWordNode.Attributes.Append(rootWordNodeAttribute);
                            XmlElement documentRoot = doc.DocumentElement;
                            ////if there is document root
                            if (null != documentRoot)
                            {
                                documentRoot.AppendChild(newRootWordNode);
                                matchingRootWordNode = newRootWordNode;
                            }
                            else
                            {
                                this.LastErrorMessage = Messages.DocumentRootNotPresent;
                                return false;
                            }                            
                        }

                            ////verify whether the derived word already exists under this root
                        bool doesDerivedWordAlreadyExist = false;
                            XmlNodeList derivedWordNodeListForMatchingRoot =
                                matchingRootWordNode.SelectNodes(XmlFileProcessorConstants.DerivedWordIdentifier);
                        if (null != derivedWordNodeListForMatchingRoot && derivedWordNodeListForMatchingRoot.Count > 0)
                        {
                            foreach (XmlNode derivedWordNode in derivedWordNodeListForMatchingRoot)
                            {
                                string derivedWord = derivedWordNode.InnerText;
                                if (string.Compare(derivedWord, word, false, CultureInfo.CurrentCulture) == 0)
                                {
                                    doesDerivedWordAlreadyExist = true;
                                    break;
                                }
                            }
                        }

                        if (!doesDerivedWordAlreadyExist)
                        {
                            ////create the derived word node also, since it does not already exist
                            XmlNode newDerivedWordNode = doc.CreateNode("element", XmlFileProcessorConstants.DerivedWordIdentifier, string.Empty);
                            newDerivedWordNode.InnerText = word;
                            matchingRootWordNode.AppendChild(newDerivedWordNode);
                        }

                        doc.Save(this.xmlFileLocation);
                        return true;
                    }
                }
            }
            catch
                (Exception ex)
            {
                this.LastErrorMessage = ex.Message;
                return false;
            }

            return false;
        }

        /// <summary>
        /// Deletes the word from root list in source XML.
        /// </summary>
        /// <param name="root">The root.</param>
        /// <param name="word">The word.</param>
        /// <returns>Whether the operation was successful</returns>
        private bool DeleteWordFromRootListInSourceXml(string root, string word)
        {
            var validationResult = this.PerformBasicValidationsOnInputParameters(root, word);
            if (!validationResult)
            {
                return false;
            }

            try
            {
                var doc = new XmlDocument();
                doc.Load(this.xmlFileLocation);
                XmlNode rootNode = doc.DocumentElement;
                if (null != rootNode)
                {
                    string xmlPathQueryToRootWord = string.Format(XmlFileProcessorConstants.XPathQueryToRoot, root);
                    XmlNode rootWordNode = rootNode.SelectSingleNode(xmlPathQueryToRootWord);
                    if (null == rootWordNode)
                    {
                        this.LastErrorMessage = Messages.RootNotPresent;
                        return false;
                    }

                    string xmlPathQueryToDerviedWord = string.Format(XmlFileProcessorConstants.XPathQueryToDerivedWord, root, word);
                    XmlNode toBeRemovedNode =
                        rootNode.SelectSingleNode(xmlPathQueryToDerviedWord);
                    if (null != toBeRemovedNode)
                    {
                        rootWordNode.RemoveChild(toBeRemovedNode);
                        doc.Save(this.xmlFileLocation);
                        return true;
                    }
                    else
                    {
                        this.LastErrorMessage = string.Format(Messages.WordNotPresent, word);
                        return false;
                    }
                }
            }
            catch (XPathException xpathEx)
            {
                this.LastErrorMessage = string.Format(Messages.XPathIncorrect, xpathEx.Message);
                return false;
            }
            catch (Exception ex)
            {
                this.LastErrorMessage = ex.Message;
                return false;
            }

            return false;
        }

        /// <summary>
        /// Updates the word in XML source.
        /// </summary>
        /// <param name="root">The root.</param>
        /// <param name="oldWord">The old word.</param>
        /// <param name="newWord">The new word.</param>
        /// <returns>Whether the operation was a success.</returns>
        private bool UpdateWordInXmlSource(string root, string oldWord, string newWord)
        {
            var validationResult = this.PerformBasicValidationsOnInputParameters(root, oldWord);
            if (validationResult && string.IsNullOrWhiteSpace(newWord))
            {
                this.LastErrorMessage = string.Format(Messages.EmptyArgument, newWord);
                validationResult = false;
            }

            if (!validationResult)
            {
                return false;
            }

            try
            {
                var doc = new XmlDocument();
                doc.Load(this.xmlFileLocation);
                XmlNode rootNode = doc.DocumentElement;
                if (null != rootNode)
                {
                    string xmlPathQueryToDerivedWord = string.Format(XmlFileProcessorConstants.XPathQueryToDerivedWord, root, oldWord);
                    XmlNode toBeUpdatedNode =
                        rootNode.SelectSingleNode(xmlPathQueryToDerivedWord);
                    if (null != toBeUpdatedNode)
                    {
                        toBeUpdatedNode.InnerText = newWord;
                        doc.Save(this.xmlFileLocation);
                        return true;
                    }
                    else
                    {
                        this.LastErrorMessage = string.Format(Messages.WordNotPresent, oldWord);
                        return false;
                    }
                }
            }
            catch (XPathException xpathEx)
            {
                this.LastErrorMessage = string.Format(Messages.XPathIncorrect, xpathEx.Message);
                return false;
            }
            catch (Exception ex)
            {
                this.LastErrorMessage = ex.Message;
                return false;
            }

            return false;
        }

        /// <summary>
        /// Parses the XML file.
        /// </summary>
        /// <returns>The list of words from the source</returns>
        private Dictionary<string, string> ParseXmlFile()
        {
            if (string.IsNullOrWhiteSpace(this.xmlFileLocation))
            {
                this.LastErrorMessage = string.Format(Messages.EmptyArgument, this.xmlFileLocation);
                return null;
            }

            if (!File.Exists(this.xmlFileLocation))
            {
                this.LastErrorMessage = string.Format(Messages.InputSourceDoesNotExist, this.xmlFileLocation);
                return null;
            }

            try
            {
                var doc = new XmlDocument();
                doc.Load(this.xmlFileLocation);
                XmlNode wordsNode = doc.SelectSingleNode(XmlFileProcessorConstants.BaseNodeIdentifier);
                if (null != wordsNode)
                {
                    XmlNodeList rootWordNodeList = wordsNode.SelectNodes(XmlFileProcessorConstants.RootNodeIdentifier);
                    if (null != rootWordNodeList && rootWordNodeList.Count > 0)
                    {
                        var wordsList = new Dictionary<string, string>();
                        foreach (XmlNode rootWordNode in rootWordNodeList)
                        {
                            if (null != rootWordNode && null != rootWordNode.Attributes)
                            {
                                string rootWord =
                                    rootWordNode.Attributes.GetNamedItem(XmlFileProcessorConstants.ValueAttributeName)
                                        .Value;
                                XmlNodeList derivedWordNodeList =
                                    rootWordNode.SelectNodes(XmlFileProcessorConstants.DerivedWordIdentifier);
                                if (null != derivedWordNodeList && derivedWordNodeList.Count > 0)
                                {
                                    foreach (XmlNode derivedWordNode in derivedWordNodeList)
                                    {
                                        string derivedWord = derivedWordNode.InnerText;
                                        if (!string.IsNullOrWhiteSpace(rootWord) &&
                                            !string.IsNullOrWhiteSpace(derivedWord))
                                        {
                                            wordsList.Add(derivedWord, rootWord);
                                        }
                                    }
                                }
                            }
                        }

                        return wordsList;
                    }
                }

                return null;
            }
            catch (Exception ex)
            {
                this.LastErrorMessage = ex.Message;
                return null;
            }
        }

        /// <summary>
        /// Performs the basic validations on input parameters.
        /// </summary>
        /// <param name="root">The root.</param>
        /// <param name="word">The word.</param>
        /// <returns>Whether the validation failed or passed</returns>
        private bool PerformBasicValidationsOnInputParameters(string root, string word)
        {
            if (string.IsNullOrWhiteSpace(root))
            {
                this.LastErrorMessage = string.Format(Messages.EmptyArgument, root);
                return false;
            }

            if (string.IsNullOrWhiteSpace(word))
            {
                this.LastErrorMessage = string.Format(Messages.EmptyArgument, word);
                return false;
            }

            if (string.IsNullOrWhiteSpace(this.xmlFileLocation))
            {
                this.LastErrorMessage = string.Format(Messages.EmptyArgument, this.xmlFileLocation);
                return false;
            }

            if (!File.Exists(this.xmlFileLocation))
            {
                this.LastErrorMessage = string.Format(Messages.InputSourceDoesNotExist, this.xmlFileLocation);
                return false;
            }

            return true;
        }
    }
}
